/**
 * File: MapLogger.js
 * Description:
 * License:
 * 
 * @author Andrea Ballatore
 */

/**
 * Constant Codes for mouse Events
 * 
 * Supported OpenLayers eventTypes: 
	0: "preaddlayer"
	1: "addlayer" 
	2: "removelayer"
	3: "changelayer" OK
	4: "movestart"
	5: "move"
	6: "moveend"
	7: "zoomend"
	8: "popupopen"
	9: "popupclose"
	10: "addmarker"
	11: "removemarker"
	12: "clearmarkers"
	13: "mouseover"
	14: "mouseout"
	15: "mousemove"
	16: "dragstart" (ignored, move instead)
	17: "drag"(ignored, move instead)
	18: "dragend"(ignored, move instead)
	19: "changebaselayer"
	20: "mousedown"
	21: "mouseup"
	22: "click"
	23: "dblclick"
	24: "rightclick"
	25: "dblrightclick"
	26: "resize"
	27: "focus"
	28: "blur"
 */

_ALL_VALID_OL_MOUSE_EVENTS = new Array( 
		"preaddlayer",
		"addlayer",
		"removelayer",
		"changelayer",
		//"mouseover",
		//"mouseout"
		"movestart", // triggered after the start of a drag, pan, or zoom
		"move", //triggered after each drag, pan, or zoom
		"moveend", //triggered after a drag, pan, or zoom completes
		"zoomend" //triggered after a zoom complete
		);

_CONTEXT_ID_PANEL_DIV_ID = "contextIdPanel";
_CONTEXT_ID_PANEL_IPHONE_DIV_ID = "statusBar";
_EVENT_ID_PANEL_IPHONE_DIV_ID = "statusBarEvents";

/**
 * Constructor of the MapLogger class, which logs all the interaction 
 * in a OpenLayers map
 * 
 * @param bMap boolean: true if page contains a map
 * @param systemId id of the 'GisSystem' defined in the Grails application. 
 * It represent a specific page with a map.
 * @param consoleId target div where to log messages from the server
 * @param consoleSessionId target div where to log messages from 
 * the server in relation to the session
 * @param queueMaxSize max size of the event queue (e.g. 200)
 * @param mapRenderer MapRenderer
 * @param bLogMouseMove boolean if true, the logger logs all the mouse movements
 * @param bIPhone true if the maprenderer is running on an iphone web page. False otherwise.
 * 
 * @return new MapLogger
 */
function MapLogger( bMap, pageId, consoleId, consoleSessionId, queueMaxSize, mapRenderer, bLogMouseMove, bIPhone ) {
	log.debug('MapLogger: contructor');
	assert(pageId, "MapLogger pageId is null");
	assert(queueMaxSize > 0 && queueMaxSize < 1000000, "MapLogger queueMaxSize invalid");
	
	this.consoleId = consoleId;
	this.consoleSessionId = consoleSessionId;
	this.eventsQueueMaxSize = queueMaxSize;
	this.pageId = pageId;
	this.bMap = bMap;
	this.bIPhone = bIPhone;
	
	this.eventCounter = 0;
	
	/**
	 * boolean flag used to stop the logging if the server
	 * side GaContext isn't ready
	 */
	this.contextReady = false;
	
	/**
	 * MapRenderer
	 */
	this.mapRenderer = mapRenderer;
	
	/**
	 * current index of packet
	 */
	this.packetCounter = 0;
	
	/**
	 * packet queue, main buffer for the events
	 * contains GuiEventPacket
	 */
	this.unsentPacketQueue = new Array();
	
	/**
	 * list of sent packets
	 * contains integers
	 */
	this.sentPacketIndexes = new Array();
	
	/**
	 * current GuiEventPacket
	 */
	this.curPacket = this.getNewPacket();
	
	/**
	 * 
	 */
	this.bLogMouseMove = bLogMouseMove;

	/** array of valid mouse events */
	this.VALID_MOUSE_EVENTS = _ALL_VALID_OL_MOUSE_EVENTS;
	
	if(this.bLogMouseMove){
		this.VALID_MOUSE_EVENTS.push("mousemove");
	}
	
	this.init();
	
	log.info(this.toString());
	
	/** used to update endTime and speed of events */
	this.lastEventWithLocation = null;
	
	assert(this.isValid(),"MapLogger: invalid");
};

/**
 * init MapLogger
 */
MapLogger.prototype.init = function() {
	log.debug("MapLogger init: log mouse movements: "+this.bLogMouseMove);
	
	//_showSpinner();
	
	// start recording events
    this.bindPageListeners();
    
    if (this.bIPhone){
    	this.bindIPhonePageListeners();
    }
    
	this.initContext();
};

/**
 * @param mapRenderer
 */
MapLogger.prototype.attachMapRenderer = function( mapRenderer ){
	assert( !this.mapRenderer, 'attachMapRenderer: this.mapRenderer should be null');
	assert(mapRenderer,'attachMapRenderer: mapRenderer is null');
	this.mapRenderer = mapRenderer;
	mapRenderer.mapLogger = this;
	this.bMap = true;
	// bind OpenLayers listeners
	if ( this.mapRenderer.olMap ){
		this.bindOpenLayerListeners( this.mapRenderer.olMap );
	}
};

/**
 * @return string contaning pretty info about the MapLogger status
 */
MapLogger.prototype.getDebugInfo = function() {
	assert(this.isValid(),"getDebugInfo: isValid false");
	info = "MapLogger: ";
	info += "unsentPacketQueue Size:" + this.unsentPacketQueue.size() +"\n";
	info += "sentPackets (last 15): ";
	
	if (this.sentPacketIndexes.size() <= 15)
		info += this.sentPacketIndexes ;
	else info += "[...]" + this.sentPacketIndexes.slice( 
			this.sentPacketIndexes.size()-15,
			this.sentPacketIndexes.size()
		);
	info += "\n";
	info += "curPacket idx: " + this.curPacket.index +" - ";
	info += "curPacket size: " + this.curPacket.size() +"";
	return info;
};

/**
 * @return string containing pretty info about the MapLogger status
 */
MapLogger.prototype.getShortDebugInfo = function() {
	assert(this.isValid(),"getShortDebugInfo: isValid false");
	var info = "unsent pck:" + this.unsentPacketQueue.size() +" - ";
	info += "curPck idx: " + this.curPacket.index;
	return info;
};

/**
 * Check consistency and existence of MapLogger member.
 * @return true if the MapLogger is valid
 */
MapLogger.prototype.isValid = function() {
	try{
		assert( this instanceof MapLogger, "MapLogger: isValid false 1" );
		assert( this.sentPacketIndexes.size() >= 0, "MapLogger: isValid false 2" );
		assert( this.unsentPacketQueue.size() >= 0, "MapLogger: isValid false 3" );
		if (this.mapRender){
			assert( this.mapRender 		 instanceof MapRenderer, "MapLogger: isValid false 4");
			assert( this.mapRender.olMap instanceof OpenLayers,  "MapLogger: isValid false 5");
		}
	} catch (e) {
		log.error("MapLogger is not valid: ");
		log.error(e);
		return false;
	}
	return true;
};

/**
 * Factory of GuiEventPacket
 * @return new Packet with index
 */
MapLogger.prototype.getNewPacket = function() {
	assert(this.isValid(), "getNewPacket: isValid false");
	assert( !isNaN(this.packetCounter), "getNewPacket this.packetCounter is not a number" );
	packet = new GuiEventPacket( this.packetCounter, this.eventsQueueMaxSize );
	this.packetCounter++;
	return packet;
};

/**
 * @return string representing MapLogger
 */
MapLogger.prototype.toString = function() {
	return this.getDebugInfo();
};

/**
 * Add event to internal queue and (if queue is full) send the queue
 * to the server and build a new queue.
 * 
 * @param e full DOM event to be logged (e.g. mousemove, mouseup, etc)
 */
MapLogger.prototype.addGuiEvent = function(e) {
	assert( this.isValid(),"addGuiEvent: MapLogger is invalid" );
	assert( e, "addGuiEvent: e null");

	if ( ! _isEventTypeValid( e.type ) )
		return;
	
	try{
		// build GuiEvent from DOM event
		newEv = GuiEventFactory(e, this.mapRenderer);
		// newEv can be null if it's not loggable
		if (!newEv) throw new Error("GuiEventFactory returned null event");
		
		// for calculating speed
		if ( newEv.getPageLocation() ){
			
			newEv.updateScreenSpeed( this.lastEventWithLocation );
			if ( !this.lastEventWithLocation ){
				log.debug("addGuiEvent: found first event with page location.");
			}
			assert( newEv.mouseInfo.speed != null, "addGuiEvent: screenSpeed is null" );
			this.lastEventWithLocation = newEv;
		}
		assert(this.curPacket,"addGuiEvent: curPacket null");
		this.eventCounter++;
		
		if ( !this.curPacket.isFull() ){
			this.curPacket.add( newEv );
		} else {
			this.resendUnsentPackets();
			
			// packet is full, try to send it
			this.sendQueueToServer( this.curPacket );
			
			// and create new packet
			this.curPacket = this.getNewPacket();
			this.curPacket.add( newEv );
		}
		
	} catch (ex) {
		log.error("addGuiEvent error");
		log.debug(ex);
		log.debug("Generated by");
		log.debug(e);
	}
};

/**
 * Try to send an unsent packet to the server again and empty cache.
 *  
 * @param e
 * @return void
 */
MapLogger.prototype.resendUnsentPackets = function(e){
	assert(this.isValid(),"resendUnsentPackets: isValid false");
	
	if (this.unsentPacketQueue.size()>0){
		var toResend = this.unsentPacketQueue.pop();
		log.debug("resendUnsentPackets: sending packet "+toResend.index+" again.");
		this.sendQueueToServer(toResend);
	}
};

/**
 * Intercept event and if it's not a map event, queue it.
 * This function prevents double calls of the same event that 
 * otherwise would be called twice on the page and on the map.
 * 
 * @param e
 * @return void
 */
MapLogger.prototype.addPageGuiEvent = function(e) {
	// fix for functional tests
	if (!e){
		log.error('addPageGuiEvent: e null');
		return;
	}
	if (this.bIPhone){
		//log.warn(e.type);
	}
	//log.warn(e);
	
	assert(e,'addPageGuiEvent: e null');
	if (this.mapRenderer){
		if (_isEventOnMap( e, this.mapRenderer.mapHtmlDivId )){
			//log.warn("map event detected");
			return;
		}
	}
	this.addGuiEvent(e);
};

/**
 * AJAX call to the server to send a GuiEventPacket. 
 * It calls sendQueueToServerSuccess or sendQueueToServerFailure
 * if the call succeded or not. 
 * @param packetToSend GuiEventPacket to be sent to the server
 */
MapLogger.prototype.sendQueueToServer = function( packetToSend ){
	assert(this.isValid(),"sendQueueToServer: isValid false");
	assert(packetToSend instanceof GuiEventPacket, "sendQueueToServer wrong type" );
	assert(packetToSend.size()>0, "sendQueueToServer: invalid size: "+packetToSend.size());
	assert(packetToSend.size()<100000, "sendQueueToServer: invalid size: "+packetToSend.size());
	
	log.info("Sending queue to server via AJAX call.");
	logConsole = $( this.consoleId );
	
	if ( !this.contextReady ){
		// call not working
		log.info("Not sending info.");
		return;
	}
	
	assert(this.contextReady,'sendQueueToServer called but contextReady is false')
	
	url = "MY_AJAX_URL_LOG_EVENTS";
	
	packetToSend.updateEndingTimes();
	var jsonCode = packetToSend.toJSON();
	assert(this.pageId, "sendQueueToServer: pageId null");
	paramsStr = 'json=' + jsonCode + '&pageId=' + this.pageId;
	logConsole.innerHTML = jsonCode;
	//alert(paramsStr);
	
	// SIMULATE SUCCESS
	this.sendQueueToServerSuccess( this, packetToSend, logConsole )
	
    /**
	 new Ajax.Request(url, {
       parameters: paramsStr,
	   method: 'post',
	   onSuccess: this.sendQueueToServerSuccess( this, packetToSend, logConsole ),
   	   onFailure: this.sendQueueToServerFailure( this, packetToSend, logConsole )
    });
	*/
};

/**
 * Called if sendQueueToServer succeeds.
 * @param that current MapLogger
 * @param packetToSend GuiEventPacket that was sent correctly
 * @param logConsole target div where to log message
 * @return void
 */
MapLogger.prototype.sendQueueToServerSuccess = function( that, packetToSend, logConsole ){
	return function(transport) {
		assert(that.isValid(),"sendQueueToServerSuccess: isValid false");
		//assert( logConsole,'sendQueueToServer console null' );
	    //assert( transport.responseText,'sendQueueToServerSuccess transport.responseText null');
	    
	    if (!transport.responseText ){
	    	that.cachePacket( packetToSend );
	    	// this is actually a failure (TODO: find out why)
	    	msg = "Oops, can't reach the server, caching packet " + packetToSend.index 
			+ "\n(Cache size: "+ that.unsentPacketQueue.size() +")";
	    	if (logConsole)
	    		_consolePrintError(logConsole,msg);
	    	log.warn(msg);
	    	return;
	    }
	    
	    // ugly way to intercept a login page in response and display a custom
	    // message
	    if (_isHtmlLoginPage( transport.responseText )){
	    	msg = "You are not logged in! Please login";
	    	if (logConsole)
	    		_consolePrintError(logConsole,msg);
	    	that.contextReady = false;
	    	log.warn(msg);
	    	if (that.bIPhone){
	    		_redirectToLogin();
	    	}
	    	return;
	    }
	    if (_isHtmlErrorPage( transport.responseText )){
	    	msg = "There was an error processing your request! It was logged on the server.";
	    	if (logConsole)
	    		_consolePrintError(logConsole,msg);
	    	_showPopupServerError(transport.responseText);
	    	return;
	    }
	    if (logConsole)
	    	_consolePrintInfo( logConsole, transport.responseText );

	    // detect errors
	    if (transport.responseText.toLowerCase().indexOf("error") >= 0){
	       // log errors
	    	_showPopupServerError(transport.responseText);
	    }
	    
	    log.debug( that.getShortDebugInfo() );
	    
	    assert( !isNaN( packetToSend.index ), "sendQueueToServerSuccess packetToSend.index is not a number" );
	    that.sentPacketIndexes.push( packetToSend.index );
	    
	    // destroy object
	    packetToSend.clear();
	    packetToSend = null;
	};
};

/**
 * Called if sendQueueToServer fails. The unsent GuiEventPacket is cached locally.
 * @param that current MapLogger
 * @param packetToSend GuiEventPacket that was not sent
 * @param logConsole target div where to log message
 * @return void
 */
MapLogger.prototype.sendQueueToServerFailure = function( that, packetToSend, logConsole ){
	return function(transport) {
		retValue = 'send Queue To Server failed, can\'t log events';
		log.error(retValue);
		
		if (transport.responseText){
			retValue = transport.responseText;
		}
		
		if (logConsole)
			_consolePrintError(logConsole, retValue);
		that.cachePacket( packetToSend );
	};
};

/**
 * Store a GuiEventPacket in a local cache.
 * @param unsentPacket GuiEventPacket to be cached locally
 * @return void
 */
MapLogger.prototype.cachePacket = function( unsentPacket ) {
	assert(this.isValid(),"cachePacket: isValid false");
	assert( unsentPacket instanceof GuiEventPacket,"cachePacket: unsentPacket wrong type");
	
	this.unsentPacketQueue.push( unsentPacket );
	
	log.warn( 'cachePacket size: ' + this.unsentPacketQueue.size() );
};

/**
 * TODO: to be moved to Utils.js
 * @return current time in milliseconds
 */
MapLogger.prototype.getTimestamp = function() {
   return (new Date()).getTime();
};

/**
 * AJAX call to the server to initialise GaContext.
 * The function also binds the MapLogger to the OpenLayers map
 * to intercept the interface events.
 * It calls initContextSuccess or initContextFailure
 * if the call succeeds or not.
 * @return void
 */
MapLogger.prototype.initContext = function() {
	   assert(this.isValid(),'initContext: invalid MapLogger');
	   log.debug("MapLogger: initContext");
	   
	   this.contextReady = false;

	   log.info("This is an AJAX call to the server to initialise event queue");
	   // if server response OK, start logging
	   
	   this.contextReady = true;
	   this.genPageStartedEvent();
	   
	   if (this.mapRenderer){
			this.genMapStartedEvent();
	   }
	   
	   /**
	   var url = 'SOME_URL/initContext?' 
		 + 'pageId='+this.pageId
		 +'&pageHost='+ _getCurrentPageHost()
		 +'&pagePath='+ _getCurrentPagePath()
		 +'&timestamp='+this.getTimestamp()
	     +'&browserInfo='+ _getBrowserInfo()
	     +'&osInfo=' + _getOSInfo();

	   new Ajax.Request(url, {
		   method: 'get',
		   onSuccess: this.initContextSuccess( this ),
	   	   onFailure: this.initContextFailure( this )
	   });
	   */
};

/**
 * Create started_map event to send the server 
 * info about the initial state of the map
 */
MapLogger.prototype.genMapStartedEvent = function(){
   assert(this.mapRenderer.olMap, 'genMapStartedEvent: mapRenderer.olMap null');
   var startMapEvent = new GuiEvent();
   startMapEvent.type = _EVENT_TYPE_MAP_STARTED;
   startMapEvent.bOnMap = true;
   this.addGuiEvent( startMapEvent );
};

/**
 * Create layer changed event to send the server 
 * adding info about active layers.
 * 
 * @node layer that was changed
 * @param activeLayers comma separated list of layers
 */
MapLogger.prototype.genLayersChangedEvent = function( node, activeLayers ){
   assert(node,"genLayerChangedEvent: node null");
   assert(this.mapRenderer.olMap, 'genLayerChangedEvent: mapRenderer.olMap null');
   assert(node.text,"genLayerChangedEvent: node.text null");
   // build object
   var event = new Object();
   event.type = _EVENT_TYPE_LAYERS_CHANGED;
   event.layer = new Object();
   event.layer.name = node.text;
   event.layer.id = "OSM Layer";
   event.property = "visibility";
   event.layer_type = _LAYER_TYPE_BASE;
   event.layer.visibility = node.checked;
   event.activelayers = activeLayers; 
   
   // add event to queue
   this.addGuiEvent( event );
   log.debug("genLayersChangedEvent: layer change event created ["+activeLayers+"]");
};

/**
 * Create started_map event to send the server 
 * info about the initial state of the map
 */
MapLogger.prototype.genPageStartedEvent = function(){
   log.debug('genPageStartedEvent');
   var startPageEvent = new GuiEvent();
   startPageEvent.type = _EVENT_TYPE_PAGE_STARTED;
   startPageEvent.bOnMap = false;
   this.addGuiEvent( startPageEvent );
};

/**
 * Called by initContext
 * @param that current Maplogger
 */
MapLogger.prototype.initContextSuccess = function( that ){
	return function(transport) {
		log.debug('context init ok');
		that.contextReady = true;
		
	    assert( transport.responseText, 'transport.responseText null');
	    assert( transport.responseText, 'transport.responseText null');
	    
	    log.debug( transport.responseText );
	    
		that.genPageStartedEvent();
	    
	    if (that.mapRenderer){
	    	that.genMapStartedEvent();
	    }   
	};
};

/**
 * Called by initContext
 * @param that current Maplogger
 */
MapLogger.prototype.initContextFailure = function( that ){
	return function(transport) {
		
		log.error('context init failed, can\'t log events');
		
		that.contextReady = false;
		
	    assert( transport.responseText, 'transport.responseText null');
	    log.error(transport.responseText);
	    consoleLog = $( that.consoleSessionId );
	    if (consoleLog){
		    consoleLog.update( transport.responseText );
		    consoleLog.setStyle({ color: '#c00' });
	    }
    	_hideSpinner();
	};
};

/**
 * Binds the MapLogger to an OpenLayers Map to catch the relevant interface
 * DOM events. 
 * 
 * @param openLayersMap an OpenLayers map
 * @return void
 */
MapLogger.prototype.bindOpenLayerListeners = function (openLayersMap) {
	assert(this.isValid(),"bindOpenLayerListeners: isValid false");
	assert(this.bMap,'bindOpenLayerListeners: bMap has to be true');
	assert( openLayersMap instanceof OpenLayers.Map, "wrong or null openLayersMap" );
	var that = this;
	try {
		// binding clicks
		log.debug("Binding clicks and double clicks");
		OpenLayers.Control.Click = OpenLayers.Class(OpenLayers.Control, {                
		    defaultHandlerOptions: {
		        'single': true,
		        'double': true,
		        'pixelTolerance': 0,
		        'stopSingle': false,
		        'stopDouble': false
		    },
		    map: openLayersMap,
		    initialize: function(options) {
		    	this.handlerOptions = OpenLayers.Util.extend(
		            {}, this.defaultHandlerOptions
		        );
		        OpenLayers.Control.prototype.initialize.apply(
		        		this, arguments
		        ); 
		        this.handler = new OpenLayers.Handler.Click(
		        	   this, {
		               'click': this.onClick,
		               'dblclick': this.onDblclick  
		            }, this.handlerOptions
		        );
		    }, 
		    onClick: function(evt) {
		    	that.addGuiEvent( evt );
		    },
		    onDblclick: function(evt) {
		    	that.addGuiEvent( evt );
		    }   
		});
		
		var control = new OpenLayers.Control.Click({
	        handlerOptions: {
	            "single": true,
	            "double": true
	        }
	    });
		control.activate();
		openLayersMap.addControl( control );
		
		// bind other events
		for ( i = 0; i < this.VALID_MOUSE_EVENTS.length; i++){
			var eventType = this.VALID_MOUSE_EVENTS[i];
			assert( eventType.length > 0, "bindOpenLayerListeners: empty eventType" );
			//log.debug("Binding event '"+eventType+"'");
			// register handlers
			openLayersMap.events.register(
				eventType, 
				openLayersMap,
				_getFunctionInContext( this, this.addGuiEvent )
			);
		}
	} catch (e) {
		log.error("bindOpenLayerListeners failed.");
		log.error(e);
	}
};

/**
 * Register listeners to catch events everywhere in the page.
 * Very important function.
 * 
 * @return void
 */
MapLogger.prototype.bindPageListeners = function() {
	log.debug( 'bindPageListeners' );
	window.onclick  		= _getFunctionInContext( this, this.addPageGuiEvent );
	window.onscroll  		= _getFunctionInContext( this, this.addPageGuiEvent );
	window.onmousewheel 	= _getFunctionInContext( this, this.addPageGuiEvent );
	document.onmousewheel 	= _getFunctionInContext( this, this.addPageGuiEvent );
	
	if (window.addEventListener){
        /** firefox code */
        window.addEventListener('DOMMouseScroll', _getFunctionInContext( this, this.addPageGuiEvent ), false);
	}
	if (this.bLogMouseMove){
		window.onmousemove  = _getFunctionInContext( this, this.addPageGuiEvent );
	}
};

/**
 * Register listeners to catch events everywhere in the page (iPhone version)
 * @return void
 */
MapLogger.prototype.bindIPhonePageListeners = function() {
	assert(this.bIPhone,'bindIPhonePageListeners: bIPhone false');
	// this needs PrototypeJS to work
	document.observe('touchstart',	_getFunctionInContext( this, this.addPageGuiEvent ) ); 
	document.observe('touchmove', 	_getFunctionInContext( this, this.addPageGuiEvent ) );
	document.observe('touchend', 	_getFunctionInContext( this, this.addPageGuiEvent ) );
};


/**
 * Util function called from the gsp pages
 * to init the maplogger
 * 
 * @param curPageId
 * @return new MapLogger
 */
function _startMapLogger( curPageId ){
	assert(curPageId,'_startPageLogger: curPageId null');
	log.debug('curPageId: '+curPageId);

	var mapLogger = new MapLogger(true, curPageId, 'jsonConsole', _CONTEXT_ID_PANEL_DIV_ID, 10, null, true);
	assert(mapLogger,"_startMapLogger: mapLogger is null");
	return mapLogger;
}

/**
 * Util function called from the gsp pages designed for the iPhone
 * to init the maplogger
 * 
 * @param curPageId
 * @return new MapLogger
 */
function _startIPhoneMapLogger( curPageId ){
	assert(curPageId,'_startIPhoneMapLogger: curPageId null');
	log.debug('curPageId: '+curPageId);

	var mapLogger = new MapLogger(false, curPageId, _EVENT_ID_PANEL_IPHONE_DIV_ID, _CONTEXT_ID_PANEL_IPHONE_DIV_ID, 5, null, false, true);
	assert(mapLogger,"_startIPhoneMapLogger: mapLogger is null");
	return mapLogger;
}